package jass.render;
import javax.sound.sampled.*;

/** Utility class for real-time audio play.
    @author Kees van den Doel (kvdoel@cs.ubc.ca)
*/
public class RTPlay extends Thread {
    private float srate;
    private int bitsPerFrame;
    private int nchannels;
    private boolean signed;
    private static final boolean bigEndian = false;
    private AudioFormat audioFormat; 
    private SourceDataLine sourceDataLine;
    private Mixer mixer;
    private static final int DEFAULT_BUFFERSIZE = 1024; //in bytes
    private int scratchBufsz; // Buffersize for writing audio
    private byte[] bbuf;
    private String preferredMixer = null;

    // for native sound
    private boolean useNative = false; // can use native audio out
    private long nativeObjectPointer = 0;
    
    /** Initialize native sound using RtAudio, setting buffersize and an internal RtAudio buffersize
        @param nchannels number of audio channels.
        @param srate sampling rate in Hertz.
        @param buffersizeJASS buffers will be rendered in these chunks
        @param nRtaudioBuffers internal buffers, 0 = lowest possiblse
        @return long representing C++ object pointer
    */
    public native long initNativeSound(int nchannels,int srate,int buffersizeJASS, int nRtaudioBuffers);

    /** write a buffer of floats to native sound.
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param nativeObjectPointer representing C++ object pointer.
        @param buf array of floats with sound buffer.
        @param buflen length of buffer.
    */
    public native void writeNativeSoundFloat(long nativeObjectPointer,float[] buf,int buflen);

    /** Close native sound
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param nativeObjectPointer representing C++ object pointer
    */
    public native void closeNativeSound(long nativeObjectPointer);

    private void findMixer() {
        Mixer.Info[] mixerinfo = AudioSystem.getMixerInfo();
        int mixerIndex = 0;
        if(preferredMixer != null) {
            for(int i=0;i<mixerinfo.length;i++) {
                // list the mixers
                System.out.println("mixer "+i+"-----------------\n");
                //System.out.println("description:"+mixerinfo[i].getDescription());
                String name = mixerinfo[i].getName();
                System.out.println("name:"+name);
                if(name.equalsIgnoreCase(preferredMixer)) {
                    mixerIndex = i;
                }
                //System.out.println("vendor:"+mixerinfo[i].getVendor());
                //System.out.println("version:"+mixerinfo[i].getVersion());
                //System.out.println("Available mixer: "+mixerinfo[i].toString());
            }
        }
	if(mixerinfo.length>0) {
	    System.out.println("CHOSEN MIXER: "+mixerinfo[mixerIndex].getName());
	    mixer = AudioSystem.getMixer(mixerinfo[mixerIndex]);
	} else {
	    System.out.println("No audio mixer found, therfore no sound.");
	    mixer = null;
	}
    }
    
    private void initAudio(int buffersize, float srate,int bitsPerFrame,int nchannels,boolean signed) {
        this.srate = srate;
        this.bitsPerFrame = bitsPerFrame;
        this.nchannels = nchannels;
        this.signed = signed;
        findMixer();
	if(mixer==null) {
	    return;
	}
        audioFormat = new AudioFormat((float)srate,bitsPerFrame,nchannels,signed,bigEndian);
        DataLine.Info info;
        if(buffersize == 0) {
            info = new DataLine.Info(SourceDataLine.class, audioFormat);
        } else {
            info = new DataLine.Info(SourceDataLine.class, audioFormat,buffersize);
        }
        //System.out.println("minBufsz="+info.getMinBufferSize()+"maxBufsz="+info.getMaxBufferSize());
        if (!mixer.isLineSupported(info)) {
            // this may be a bogus message, e.g. on Tritonus this is not reliable information
            System.out.println(getClass().getName()
                               +" : Error: sourcedataline: not supported (this may be a bogus message under Tritonus)\n");
        }       
        try {
            sourceDataLine = (SourceDataLine) mixer.getLine(info);
            if(buffersize == 0) {
                sourceDataLine.open(audioFormat);
            } else {
                sourceDataLine.open(audioFormat,buffersize);
            }
            sourceDataLine.start();
        } catch (LineUnavailableException ex) {
            System.out.println(getClass().getName()+" : Error getting line\n");
        }
        scratchBufsz = 4; // some small value. Will be increased when needed
        bbuf = new byte[scratchBufsz];
    }
    
    private void initAudioNative(float srate,int nchannels,int buffersizeJASS) { 
        int numRtAudioBuffers = 0;
	// rtaudio has different convention regarding nchannels and buffersize; that's why the /nchannels
        initAudioNative(srate,nchannels,buffersizeJASS/nchannels,numRtAudioBuffers);
    }

    private void initAudioNative(float srate,int nchannels,int buffersizeJASS,int numRtAudioBuffers) { 
        this.srate = srate;
        this.bitsPerFrame = bitsPerFrame;
        this.nchannels = nchannels;
        this.signed = signed;
        scratchBufsz = 4*2;
        // get pointer to C++ object
	// rtaudio has different convention regarding nchannels and buffersize; that's why the /nchannels
        nativeObjectPointer = initNativeSound(nchannels,(int)srate,buffersizeJASS/nchannels,numRtAudioBuffers); 
        
        //This will ensure that close() gets called before the program exits
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                System.out.println("The shutdown hook in RTPlay is executing");
                close();
                try{	
                    sleep(100);
                } catch(Exception e) {
                    System.out.print("The sleep function is broken");
                }
            }
	    });
	
    }

    /** Constructor.
        @param bufferSize Buffer size used by JavaSound.
        @param srate sampling rate in Hertz.
        @param bitsPerFrame number of bits per audio frame.
        @param nchannels number of audio channels.
        @param signed true if signed format, false otherwise.
    */
    public RTPlay(int buffersize, float srate,int bitsPerFrame,int nchannels,boolean signed) {
        initAudio(buffersize,srate,bitsPerFrame,nchannels,signed);
    }


    /** Constructor. Chose preferred mixer by name
        @param bufferSize Buffer size used by JavaSound.
        @param srate sampling rate in Hertz.
        @param bitsPerFrame number of bits per audio frame.
        @param nchannels number of audio channels.
        @param signed true if signed format, false otherwise.
        @param preferredMixer preferred mixer as a name string
    */
    public RTPlay(int buffersize, float srate,int bitsPerFrame,int nchannels,
                  boolean signed,String preferredMixer) {
        this.preferredMixer = preferredMixer;
        initAudio(buffersize,srate,bitsPerFrame,nchannels,signed);
    }

    /** Constructor. Uses default high latency JavaSound buffer size.
        @param srate sampling rate in Hertz.
        @param bitsPerFrame number of bits per audio frame.
        @param nchannels number of audio channels.
        @param signed true if signed format, false otherwise.
    */
    public RTPlay(float srate,int bitsPerFrame,int nchannels,boolean signed) {
        initAudio(0,srate,bitsPerFrame,nchannels,signed);
    }
        
    /** Constructor. Uses native audio write.
        Needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param srate sampling rate in Hertz.
        @param nchannels number of audio channels.
        @param buffersizeJass this will be the buffersize of RtAudio
    */
    public RTPlay(float srate,int nchannels,int buffersizeJass) {
        useNative = true;
        // load shared library with native sound implementations
        try {
            System.loadLibrary("rtaudio");
        } catch(UnsatisfiedLinkError e) {
            System.out.println("Could not load shared library rtaudio: "+e);
        }
        initAudioNative(srate,nchannels,buffersizeJass);
    }

        
    /** Constructor. Uses native audio write. Also specify RtAudio tweak parameter numberofbuffers used
        Needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param srate sampling rate in Hertz.
        @param nchannels number of audio channels.
        @param buffersizeJass jass buffersize (TODO fix name)
        @param numRtAudioBuffers number of rtaudio buffers (0 is lowest)
    */
    public RTPlay(float srate,int nchannels,int buffersizeJass,int numRtAudioBuffers) {
        useNative = true;
        // load shared library with native sound implementations
        try {
            System.loadLibrary("rtaudio");
        } catch(UnsatisfiedLinkError e) {
            System.out.println("Could not load shared library rtaudio: "+e);
        }
        initAudioNative(srate,nchannels,buffersizeJass,numRtAudioBuffers);
    }


    /** Write audio buffer to output queue and block if queue is full.
        @param y output buffer.
    */
    public void write(float [] y) {
	if(mixer==null) {
	    return;
	}
        if(useNative) {
            writeNativeSoundFloat(nativeObjectPointer,y,y.length);
        } else {
            if(2 * y.length > scratchBufsz) {
                scratchBufsz = 2 * y.length;
                bbuf = new byte[scratchBufsz];
            }
            FormatUtils.floatToByte(bbuf,y);
            sourceDataLine.write(bbuf,0,2*y.length);
        }
    }
    
    /** Close line or native sound
     */
    public void close() {
        if(useNative) {
            System.out.println("RTPlay.close() will call closeNativeSound: nativeObjectPointer= "+nativeObjectPointer);
            if(nativeObjectPointer != 0) {
                closeNativeSound(nativeObjectPointer);
                nativeObjectPointer = 0;
            }
            try{
                sleep(100);
		    } catch(Exception e) {
		    }
        } else {
            sourceDataLine.stop();
            sourceDataLine.close();
        }
    }
    
}





